﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public class Sequence<TContext, TDatabase, TParentDbEntity, TParentDbValue, TChildDbEntity, TParentOutput, TChildOutput, TOutputValue> : IDbValueMapping<TDatabase, TParentDbValue>
        where TContext : MappingContext
        where TChildDbEntity : class
    {
        private Expression<Func<TParentDbEntity, TParentDbValue>> dbSelector;
        private Expression<Func<TParentOutput, TOutputValue>> outputSelector;
        private Mapper<TContext, TDatabase, TChildDbEntity, TChildOutput> mapper;
        private PropertyInfo property;

        public Sequence(Expression<Func<TParentDbEntity, TParentDbValue>> dbSelector, Expression<Func<TParentOutput, TOutputValue>> outputSelector, Mapper<TContext, TDatabase, TChildDbEntity, TChildOutput> mapper)
        {
            this.dbSelector = dbSelector;
            this.outputSelector = outputSelector;
            this.mapper = mapper;
        }

        public PropertyInfo TransientProperty
        {
            get { return property; }
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            mapper.Initialize();
            property = type.DefineProperty(mapper.TransientType.Name, mapper.TransientType.MakeArrayType());
        }

        public void DefineTransientInitialization(Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            property = transientType.GetProperty(property.Name);

            var lambdaBinder = new LambdaBinder();
            var boundDbSelector = lambdaBinder.Bind(dbSelector, parameterExpression);

            var entitySet = boundDbSelector.Body;
            var subSelector = mapper.transientSelector;

            var transientSelectMethod = typeof(Enumerable).GetGenericMethod("Select", new[] { typeof(TChildDbEntity), mapper.TransientType }, new[] { typeof(IEnumerable<TChildDbEntity>), subSelector.Type }); 
            var transientToArrayMethod = typeof(Enumerable).GetGenericMethod("ToArray", new[] { mapper.TransientType }, new[] { typeof(IEnumerable<>).MakeGenericType(mapper.TransientType) }); 

            var callSelect = Expression.Call(transientSelectMethod, entitySet, subSelector);
            var callToArray = Expression.Call(transientToArrayMethod, callSelect);
            var initializer = Expression.Bind(property.GetSetMethod(), callToArray);
            initializers.Add(initializer);
        }

        void IMapping.OnTransientInitialized(object db, Array transient)
        {
        }

        public void InitializeOutput(object transient, object output)
        {
            var outputProperty = outputSelector.GetPropertyInfo();
            var subValue = (Array)property.GetValue(transient, null);

            var outputValue = mapper.TransformToOutput(subValue);
            object value = outputValue;
            if (outputProperty.PropertyType.IsGenericList())
                value = outputValue.ToList();

            outputProperty.SetValue(output, value, null);
        }
    }
}